The ability to install custom
component services in .NET is a major advancement for software
engineering and component-oriented programming. Custom component
services allow you to fine-tune and optimize the way .NET services your
particular application and business logic. Custom component services
decouple clients from objects, because they don't need to coordinate the
execution of the custom service; you can focus on implementing the
business logic, rather than the service. Examples of custom services
include application logging and tracing, performance counters, custom
thread management, filtering of method calls, parameter checks, event
subscriptions, and so on.
Custom component services are provided in the form of custom context attributes. Ordinary custom attributes have no use unless you provide the reflection code to look for these
attributes, interpret their values, and act upon them. .NET is
indifferent to such custom attributes. Unlike generic custom attributes,
.NET is very much aware of custom context attributes when they are used on context-bound objects. Context attributes must derive from the class ContextAttribute, defined in the System.Runtime.Remoting.Contexts
namespace. When creating a new context-bound object, .NET reflects the
object's metadata and places it in the appropriate context based on the
behavior of the attributes. Custom context attributes can affect the
context in which the object is activated and can be used to install all
four types of message sink interceptors. The next two sections
demonstrate how to build custom context attributes and component
services. First, you will see how to develop a custom context attribute
and how it affects the activation context; then you'll look at how to
install custom message sinks. Finally, you'll walk though the
development of two real-life, useful custom component services.
Custom context
attributes and custom message sinks are undocumented features of .NET.
Microsoft is committed to supporting contexts in future versions of
.NET, but not to extending the infrastructure and adding features. |
|
1. Building a Custom Context Attribute
Each context has a set of properties associated with
it. The properties are the component services the context supports. A
context-bound object shares a context with its client only if the
client's context has all the services the component requires—in other
words, if the context has the required properties. If the client's
context doesn't have one or more of the properties the object requires,
.NET creates a new context and puts the object in it. In addition, a
context property may require a new context regardless of the properties
of the client's context. You use context attributes to specify the
required services. The context attributes are those that decide whether
or not the client's context is sufficient.
To understand how context attributes affect context activation, consider a custom context attribute that adds a color property to a context. The color is an enum of the type ColorOption:
public enum ColorOption{Red,Green,Blue};
You use ColorAttribute as a class attribute on a class derived from ContextBoundObject:
[Color(ColorOption.Blue)]
public class MyClass: ContextBoundObject
{...}
Obviously, a color property isn't much of a service, but it's a good example. .NET creates objects of the class MyClass in the client's context only if the creating client's context has a color property and if its value is set to ColorOption.Blue. Otherwise, .NET creates a new context, lets the attribute set its color property to ColorOption.Blue, and places the new object in the new context. ColorAttribute also has a default constructor, setting the context color to ColorOption.Red:
[Color]//Default is ColorOption.Red
public class MyClass: ContextBoundObject
{...}
Example 1 shows the implementation of the ColorAttribute custom context attribute.
Example 1. The ColorAttribute custom context attribute
using System.Runtime.Remoting.Contexts; using System.Runtime.Remoting.Activation; public enum ColorOption {Red,Green,Blue}; [AttributeUsage(AttributeTargets.Class)] public class ColorAttribute : ContextAttribute { ColorOption m_Color; public ColorAttribute( ) : this(ColorOption.Red)//Default color is red {} public ColorAttribute(ColorOption color) : base("ColorAttribute") { m_Color = color; } //Add a new color property to the new context public override void GetPropertiesForNewContext(IConstructionCallMessage ctor) { IContextProperty colorProperty = new ColorProperty(m_Color); ctor.ContextProperties.Add(colorProperty); } //ctx is the creating client's context public override bool IsContextOK(Context ctx,IConstructionCallMessage ctorMsg) { ColorProperty contextColorProperty = null; //Find out if the creating context has a color property. If not, reject it contextColorProperty = ctx.GetProperty("Color") as ColorProperty; if(contextColorProperty == null) { return false; } //It does have a color property. Verify color match return (m_Color == contextColorProperty.Color); } } //The ColorProperty is added to the context properties collection by the //ColorAttribute class public class ColorProperty : IContextProperty { ColorOption m_Color; public ColorProperty(ColorOption ContextColor) { Color = ContextColor; } public string Name { get { return "Color"; } } //IsNewContextOK called by the runtime in the new context public bool IsNewContextOK(Context ctx) { ColorProperty newContextColorProperty = null; //Find out if the new context has a color property. If not, reject it newContextColorProperty = ctx.GetProperty("Color") as ColorProperty; if(newContextColorProperty == null) { return false; } //It does have color property. Verify color match return (Color == newContextColorProperty.Color); } public void Freeze(Context ctx) {} //Color needs to be public so that the attribute class can access it public ColorOption Color { get { return m_Color; } set { m_Color = value; } } }
|
ColorAttribute has a member called m_Color
that contains the required context color. The color is specified during
the attribute construction, either explicitly or by using the default
constructor. As a custom context attribute, it derives from ContextAttribute. The single constructor of ContextAttribute requires a string naming the new context attribute. This is provided by a call to the ContextAttribute constructor in the ColorAttribute constructor:
public ColorAttribute(ColorOption color) : base("ColorAttribute")
{...}
ContextAttribute derives from and provides a virtual implementation of the IContextAttribute interface, defined as:
public interface IContextAttribute
{
void GetPropertiesForNewContext(IConstructionCallMessage msg);
bool IsContextOK(Context ctx,IConstructionCallMessage msg);
}
The IsContextOK( ) method lets the context attribute examine the creating client's context, which is provided in the ctx
parameter. If the client's context is adequate, no further action is
required, and .NET activates the new object in the creating client's
context. If the context attribute returns false from IsContextOK( ), .NET creates a new context and calls GetPropertiesForNewContext( ),
letting the context attribute add new properties to the new context.
Because a single object can have more than one context attribute, .NET
can optimize its queries of the attributes. .NET starts iterating over
the attribute list, calling IsContextOK( ) on each one. As soon as it finds an attribute in the list that returns false, .NET aborts the iteration and creates a new context. It then calls GetPropertiesForNewContext( ) on each context attribute, letting it add its properties to the new context. ColorAttribute needs to override both methods of IContextAttribute and manage its single context property. Context properties are objects that implement the IContextProperty interface:
public interface IContextProperty
{
string Name{ get; }
void Freeze(Context newContext);
bool IsNewContextOK(Context newCtx);
}
Each context property is identified by name via the Name property of IContextProperty. ColorAttribute uses a helper class called ColorProperty to implement IContextProperty. ColorProperty names itself as "Color". ColorProperty also provides the Color public property of type ColorOption. This allows for type-safe checking of the color value.
In its implementation of IsContextOK( ), ColorAttribute checks whether the client's context has a property called "Color". If it doesn't, IsContextOK( ) returns false. If the client's context has a color property, ColorAttribute verifies that there is a color match by comparing the value of the color property with its own color.
The implementation of GetPropertiesForNewContext( ) is straightforward as well: the single parameter is an object of type IConstructionCallMessage, providing a collection of properties for the new context via the ContextProperties property. ColorAttribute creates an object of type ColorProperty, initializes it with the required color, and adds it to the collection of properties for the new context.
Because a single
context-bound object can have multiple context attributes, it's possible
that some will conflict with others. To handle such an eventuality,
after adding all the properties to the new context, .NET calls IsNewContextOK( ) on each property. If a property returns false, .NET aborts creating the new object and throws an exception of type RemotingException. In IsNewContextOK( ), ColorAttribute simply verifies that the new context has the correct color. The Freeze( ) method lets a context property know that the final location of the context is established and available for advanced use only.
Figure 1
is a UML activity diagram summarizing the process flow when using a
custom context attribute and a context property. The diagram shows the
order in which the various methods take place and the resulting
activation logic.
2. Installing a Custom Message Sink
To provide a useful component service, the custom context attribute must install at least one custom message sink. The message sink can be either a server context sink,
a client context sink, an envoy sink, or a server object sink.
Commonly, a custom context attribute installs only a server context
sink. The other sinks are intended for advanced cases, but you can
install one if the need arises. For each type of custom sink you wish to
contribute to the interception chain, the custom context property must
implement a matching interface.
2.1. Providing a server context sink
To contribute a server context sink, the custom context property needs to implement the IContributeServerContextSink interface, defined as:
public interface IContributeServerContextSink
{
IMessageSink GetServerContextSink(IMessageSink nextSink);
}
In its implementation of GetServerContextSink( ),
the context property creates a sink object and concatenates it to the
next sink in the chain, which is provided as the method parameter. GetServerContextSink( ) should return the new sink it created so that .NET can add it to the interception chain. For example, here is how to install GenericSink (presented in Example 11-2) as a server context sink:
public IMessageSink GetServerContextSink(IMessageSink nextSink)
{
IMessageSink sink = new GenericSink(nextSink);
return sink;
}
The server context sink intercepts all calls coming into the context. .NET calls GetServerContextSink( ) after its call to IContextProperty.IsNewContextOK( )
and before creating the object, allowing the context property to
provide the sink. A server context sink can intercept construction
calls.
2.2. Providing a client context sink
To install a client context sink, the context property needs to implement the IcontributeClientContextSink interface, defined as:
public interface IContributeClientContextSink
{
IMessageSink GetClientContextSink(IMessageSink nextSink);
}
A client context sink
affects the context-bound object only when it's the client of another
object outside the context; it intercepts all calls exiting the context.
.NET calls GetClientContextSink( )
only when the object makes its first call outside the context. The
information in the message object passed to the sink pertains to the
target object, not the client.
2.3. Providing an envoy sink
The context property can also implement the IContributeEnvoySink interface, defined as:
public interface IContributeEnvoySink
{
IMessageSink GetEnvoySink(MarshalByRefObject obj,IMessageSink nextSink);
}
In this case, when a
proxy to an object on the client's side is set up, the proxy has an
envoy sink as part of the interception chain leading to that object. The
envoy sink intercepts all calls going from the client to the object.
Other objects accessed by the client aren't affected. Every time a new
client in a different context connects to the object, .NET installs an
envoy sink in that client's context. .NET calls GetEnvoySink( )
after creating the new object but before returning control to the
client. You can't intercept construction calls with an envoy sink.
2.4. Providing an object sink
To install a server object sink, the context property needs to implement the IContributeObjectSink interface, defined as:
public interface IContributeObjectSink
{
IMessageSink GetObjectSink(MarshalByRefObject obj,IMessageSink nextSink);
}
The object sink is
installed on an object-by-object basis, which means it intercepts calls
only to the object whose reference is provided in the GetObjectSink( ) call. Other calls into the context aren't affected. .NET calls GetObjectSink( )
before the first method call is forwarded to the object. As a result,
you can't intercept construction calls with an object sink.
2.5. Processing messages
The IMessage
interface presented previously is a collection of information about the
method being intercepted. Although you can retrieve that information
from the dictionary, there is a better way. When you intercept an
incoming call, the different message objects (used for synchronous
methods, asynchronous methods, and constructor calls) all support the IMethodMessage interface, defined as:
public interface IMethodMessage : IMessage
{
int ArgCount{ get; }
object[] Args{ get; }
bool HasVarArgs{ get; }
LogicalCallContext LogicalCallContext { get; }
MethodBase MethodBase { get; }
string MethodName{ get; }
object MethodSignature{ get; }
string TypeName{ get; }
string Uri{ get; }
object GetArg(int argNum);
string GetArgName(int index);
}
IMethodMessage
provides information about the method name, its arguments, the type on
which the method was called, and the object's location. You can use that
information in your pre-call message-processing logic. After the last
sink—the stack builder—invokes the call on the object, it returns a
different message object. Again, there are several types of returned
method objects, but they are all polymorphic with the IMethodReturnMessage interface, defined as:
public interface IMethodReturnMessage : IMethodMessage
{
Exception Exception { get; }
int OutArgCount { get; }
object[] OutArgs { get; }
object ReturnValue { get; }
object GetOutArg(int argNum);
string GetOutArgName(int index);
}
IMethodReturnMessage derives from IMethodMessage
and provides additional information about the method's returned value,
the values of any outgoing parameters, and any exceptions. The fact that
exception information is captured is of particular interest. If the
object throws an exception, the stack-builder sink silently catches it
and saves it in the returned message object. This allows all the sinks
up the call chain to examine the exception object. When control returns
to the proxy, if exception information is present, the proxy re-throws
it on the calling client's side.